/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package parserxml;

import net.n3.nanoxml.*;
import java.io.*;
import java.util.*;
import org.exolab.castor.xml.dtd.parser.*;
import org.exolab.castor.xml.dtd.*;

/**
 */
public class Main {
    //Lista global con todas las tablas generadas a partir del DTD.

    static HashMap<String, Tabla> listaTablas = new HashMap<String, Tabla>();
    static HashMap<String, RelacionER> relacionesER = new HashMap<String, RelacionER>();

    public static void main(String[] args) throws Exception {

        FileInputStream inputStream = null;
        InputStreamReader reader = null;
        InputCharStream charStream = null;
        DTDParser parser;
        DTDdocument dtd = new DTDdocument();

        try {
            inputStream = new FileInputStream(args[0]);
            reader = new InputStreamReader(inputStream, "UTF-8");
            charStream = new InputCharStream(reader);
            parser = new DTDParser(charStream);
            dtd = parser.Input(); //Esta es la llamada que parsea el archivo
        } catch (Exception e) {
            System.out.println("Error iniciando el parser.");
            e.printStackTrace();
        }

        Enumeration enumelem = dtd.getElements();

        while (enumelem.hasMoreElements()) {
            Element e = (Element) enumelem.nextElement();
            procesarElemento(e, dtd);
        }
        generarScript();

        // FUNCION PARA LEER EL XML Y GENERAR SCRIPT DE INSERCIONES
        readXML("contenido.xml");
    }

    public static void imprimirTablas() {

        Collection<Tabla> tablas = listaTablas.values();
        Iterator<Tabla> iterTabla = tablas.iterator();

        while (iterTabla.hasNext()) {
            Tabla tabla = iterTabla.next();
            System.out.println(tabla.getNombre() + ":");
            Iterator columnas = tabla.getAllAtributos();
            while (columnas.hasNext()) {
                Atributo c = (Atributo) columnas.next();
                System.out.println("    " + c.getNombre());
            }
        }
    }

    /**
     * @return Escribe en un archivo de nombre Schema.sql, un script
     * que genera todas las tablas guardadas en listaTablas (global).
     * @throws IOException
     */
    public static void generarScript() throws IOException {

        Writer out = new FileWriter("Schema.sql");

        Collection<Tabla> tablas = listaTablas.values();
        Iterator<Tabla> iterTabla = tablas.iterator();

        while (iterTabla.hasNext()) {
            Tabla tabla = iterTabla.next();
            out.write("CREATE TABLE " + tabla.getNombre() + "(\n");

            Iterator<Atributo> iterAtributo = tabla.getAllAtributos();
            while (iterAtributo.hasNext()) {
                Atributo attr = iterAtributo.next();

                // si el atributo es auto-increment, entonces su nombre termina
                // por '_AI'
                if (attr.getNombre().endsWith("_AI")) {
                    out.write("\t" + attr.getNombre() + " int NOT NULL AUTO_INCREMENT");
                } // si el atributo no es auto-increment se hace lo usual
                else {
                    out.write("\t" + attr.getNombre() + " VARCHAR(100)");
                    if (!attr.getIsNull()) {
                        out.write(" NOT NULL");
                    }
                }
                // termina la linea del atributo con una coma y salto de linea
                out.write(",\n");

                // si el atributo apunta a otra tabla entonces se construye
                // y agrega la restriccion al script
                if (attr.getClaveForanea() != null) {
                    out.write(processIDRefs(attr.getClaveForanea()));
                }
            }
            // al final de la tabla se coloca la restriccion de la
            // clave primaria
            if (!tabla.getClavePK().isEmpty()) {
                out.write("\tPRIMARY KEY (" + tabla.getClavePK() + ")\n");
            }
            // termina la tabla con parentesis y dos saltos de linea
            out.write(");\n\n");
        }
        out.close();
    }

    static void processElement(Element e, DTDdocument dtd) {

        Element elemhijo;

        if (esEntidad(dtd, e)) {

            procesarEntidad(dtd, e);
            Tabla tabla = new Tabla(e.getName());
            listaTablas.put(e.getName(), tabla);
            processAtributes(e.getAttributes(), tabla);


            if (esPcdata(e)) {
                processPcdataAttribute(e, tabla);       //Procesa el elemento.
            } else if (e.isElemOnlyContent()) {

                //System.out.println("Entro con: "+e.getName());
                ContentParticle hijo = e.getContent();
                if (hijo.isReferenceType()) {

                    CasosElems(dtd, e, hijo);
                } else if (hijo.isSeqType()) {

                    Enumeration hijos = hijo.getChildren();
                    while (hijos.hasMoreElements()) {
                        ContentParticle h = (ContentParticle) hijos.nextElement();
                        CasosElems(dtd, e, h);
                    }
                }
                //Procesar contenido del elemento.
                //process
            }
        }
        //if (esRelacion(e)) {
        //}
    }

    static void procesarElemento(Element e, DTDdocument dtd) {

        if (esEntidad(dtd, e)) {
            procesarEntidad(dtd, e);
        }

        if (esRelacion(dtd, e) == 1 || esRelacion(dtd, e) == -1) {
            procesarRelacion(e, dtd);
        }
    }

    static void procesarRelacion(Element e, DTDdocument dtd) {

        String nombrehijo;
        boolean puedenull;
        Tabla relacion = new Tabla(e.getName());
        listaTablas.put(e.getName(), relacion);

        ContentParticle cp = e.getContent();
        Enumeration hijosrel = cp.getChildren();
        while (hijosrel.hasMoreElements()) {

            ContentParticle cphijos = (ContentParticle) hijosrel.nextElement();
            nombrehijo = cphijos.getReference();
            Element hijo = dtd.getElement(nombrehijo);
            puedenull = cphijos.isZeroOrOneOccurance();
            //El elemento hijo es PCDATA, o sea es atributo de la relacion,
            //entonces lo agrego a la tabla de la relacion.
            if (esPcdata(hijo) && hijo.getAttributes() == null) {

                Atributo a = new Atributo(nombrehijo, puedenull);
                relacion.addAtributo(nombrehijo, a);
            } else if (esEntidad(dtd, hijo)) {

                Atributo cf = new Atributo(hijo.getName() + "_FK", false, hijo.getName());
                relacion.addAtributo(hijo.getName() + "_FK", cf);
            }
        }
    }

    /***
     * Procesa los elementos que sean una entidad.
     * @param Element e
     */
    public static void procesarEntidad(DTDdocument dtd, Element e) {

        Tabla tabla = new Tabla(e.getName());
        listaTablas.put(e.getName(), tabla);
        processAtributes(e.getAttributes(), tabla);

        if (tieneHijos(e) && e.getContent().isSeqType()) {

            Enumeration<ContentParticle> hijos = e.getContent().getChildren();
            while (hijos != null && hijos.hasMoreElements()) {

                ContentParticle hijo = hijos.nextElement();
                procesarAtributo(dtd, tabla, hijo);
            }
        } else if (tieneHijos(e)) {
            ContentParticle hijo = e.getContent();
            procesarAtributo(dtd, tabla, hijo);
        }
    }

    public static void procesarAtributo(DTDdocument dtd, Tabla tabla, ContentParticle attr) {

        if (attr.isReferenceType()) {

            if (attr.isZeroOrOneOccurance()) {
                Atributo attributo = new Atributo(attr.getReference(), true);
                tabla.addAtributo(attr.getReference(), attributo);
            } else if (attr.isOneOccurance()) {
                Atributo attributo = new Atributo(attr.getReference(), true);
                tabla.addAtributo(attr.getReference(), attributo);
            }
        }
    }

    /**
     * Llena el vector compuesto con el atributo compuesto aplanado.
     * @param dtd
     * @param compuesto
     * @param cp
     */
    public static void aplanarAtributo(DTDdocument dtd, Vector compuesto,
            ContentParticle cp) {

        if (cp.isReferenceType()) {

            Enumeration<ContentParticle> hijos = cp.getChildren();
            while (hijos != null && hijos.hasMoreElements()) {

                ContentParticle hijo = hijos.nextElement();
                if (hijo.isReferenceType()) {
                    compuesto.add(hijo.getReference());
                }
                aplanarAtributo(dtd, compuesto, hijo);
            }
        }
    }

    /**
     *
     * @return true si es un atributo compuesto.
     */
    public static boolean esAtributoCompuesto(DTDdocument dtd, Element e) {

        if (esEntidad(dtd, e) || tieneId(e) || !tieneHijos(e)) {
            return false;
        }
        if (tieneHijos(e)) {

            Enumeration<ContentParticle> hijos = e.getContent().getChildren();
            while (hijos != null && hijos.hasMoreElements()) {

                ContentParticle hijo = hijos.nextElement();
                if (!esAtributoCompuesto(dtd, hijo)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     *
     * @return true si es un atributo compuesto.
     */
    public static boolean esAtributoCompuesto(DTDdocument dtd, ContentParticle cp) {

        if (cp.isReferenceType()) {

            Element e = dtd.getElement(cp.getReference());
            if (esEntidad(dtd, e) || tieneId(e) || !tieneHijos(cp)) {
                return false;
            } else if (!tieneId(e) && e.isElemOnlyContent()) {

                Enumeration<ContentParticle> hijos = cp.getChildren();
                while (hijos != null && hijos.hasMoreElements()) {

                    ContentParticle hijo = hijos.nextElement();
                    Element elemHijo = dtd.getElement(e.getName());
                    if (!esAtributoCompuesto(dtd, elemHijo)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * @param dtd
     * @param cp
     * @return verdadero si el atributo es simple.
     */
    public static boolean esAtributoSimple(DTDdocument dtd, ContentParticle cp) {

        Element e;
        if (cp.isReferenceType()) {
            e = dtd.getElement(cp.getReference());

            if (esEntidad(dtd, e) || tieneId(e)) {
                return false;
            } else if (esPcdata(cp)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param dtd
     * @param cp
     * @return verdadero si el atributo es simple.
     */
    public static boolean esAtributoSimple(DTDdocument dtd, Element e) {

        if (esEntidad(dtd, e) || tieneHijos(e)) {
            return false;
        }

        if (esPcdata(e) && esHijo(dtd, e) && !tieneHijos(e)) {
            return true;
        }
        return false;
    }

    /***
     *
     * @param Element e
     * @return Cierto si el elemento se traduce a una tabla en relacional.
     */
    public static boolean esTabla(Element e) {
        //Si tiene puros elementos que no son #PCDATA.
        if (e.isElemOnlyContent()) {
            //Verifica si tiene o no hijos.
            if (e.getContent().getChildren() != null || e.getContent().isReferenceType()) {
                return true;
            }
        } //Si tiene atributos, tambien puede ser una
        //tabla.
        else if (e.getAttributes().hasMoreElements()) {
            return true;
        }
        return false;
    }

    /**
     * @param Elemento e
     * @return retorna true si el elemento e tiene hijos.
     */
    public static boolean tieneHijos(Element e) {

        if (e.isElemOnlyContent()) {
            return true;
        }
        return false;
    }

    /**
     * @param Elemento e
     * @return retorna true si el elemento e tiene hijos.
     */
    public static boolean tieneHijos(ContentParticle cp) {

        if (true) {
            return true;
        }
        return false;
    }

    /**
     * 
     * @return true si el elemento e tiene id.
     */
    public static boolean tieneId(Element e) {

        Enumeration<Attribute> atributos = e.getAttributes();
        while (atributos.hasMoreElements()) {

            Attribute atributo = atributos.nextElement();
            if (atributo.isIDType()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param e Element
     * @return true si el elemento se considera como una entidad.
     */
    public static boolean esEntidad(DTDdocument dtd, Element e) {

        if (tieneId(e) && tieneHijos(e)) {

            Enumeration<ContentParticle> hijos = e.getContent().getChildren();
            while (hijos != null && hijos.hasMoreElements()) {

                ContentParticle hijoParticle = hijos.nextElement();
                if (hijoParticle.isReferenceType()) {

                    Element hijo = dtd.getElement(hijoParticle.getReference());
                    if (esEntidad(dtd, hijo)) {
                        return false;

                    }
                }
            }
            return true;
        }
        return false;
    }

    /**
     * 
     * @param Enumeration (lista de atributos.)
     */
    public static void processAtributes(Enumeration attributes, Tabla tabla) {

        Atributo atributo;
        Attribute attrDTD;

        boolean multivaluado;

        //Recorre todos los atributos.
        while (attributes.hasMoreElements()) {
            attrDTD = (Attribute) attributes.nextElement();
            atributo = new Atributo(attrDTD.getName(), attrDTD.isIMPLIED());
            //Si tiene un atributo como clave primaria.

            if (attrDTD.isIDType()) {
                HashSet<String> clavesAttr = new HashSet<String>();
                // TODO hay que agregar las claves primarias del padre
                clavesAttr.add(attrDTD.getName());
                tabla.setClavePK(clavesAttr);
                atributo.setIsNull(false);
            } //Si tiene un atributo como clave foranea.
            else if (attrDTD.isIDREFType()) {
                atributo.setNombre(attrDTD.getName() + "_FK");
                atributo.setClaveForanea(attrDTD.getName());
            } //Construye un nuevo atributo y lo agrega a la tabla
            //correspondiente.
            tabla.addAtributo(attrDTD.getName(), atributo);
        }
        // En caso que ningun atributo del ATTLIST sea de tipo ID
        // entonces la tabla no tiene clavePK, por lo que le colocamos
        // un atributo adicional de tipo auto-increment para que este 
        // sea la clave primaria
//        if (tabla.getClaves().isEmpty()) {
//            atributo = new Atributo(tabla.getNombre() + "_AI", false);
//            tabla.addAtributo(atributo.getNombre(), atributo);
//            tabla.setClaves(atributo.getNombre());
//        }
    }

    /**
     * CASO ESPECIFICO DE VICKY NUMERO 1.
     * Cuando el elemento hijo no es tabla y es * o + entonces:
     * Se crea una tabla para el hijo (si no la tiene aun) y se le agrega
     * una clave foranea al padre.
     */
    public static void CasosElems(DTDdocument dtd, Element padre, ContentParticle cp) {

        //System.out.print(padre.getName());
        String nombre = cp.getReference();
        Element e = dtd.getElement(nombre);
        Tabla tabla;
        //REVISO CASOS.
        //Si no es tabla y no es * o +

        if (!esTabla(e)
                && (cp.isOneOrMoreOccurances() || cp.isZeroOrMoreOccurances())) {
            if (!listaTablas.containsKey(cp.getReference())) {
                //Crear la tabla si no esta
                tabla = new Tabla(e.getName());
                listaTablas.put(e.getName(), tabla);


            }
            //Ponerle clave foranea en la tabla del cp apuntando al papa
            // TODO revisar como colocar las claves foraneas
//            Atributo clavePK = new Atributo(e.getName() + "_AI", false);
//            Atributo contenido = new Atributo(e.getName(), true);
//            Atributo claveFK = new Atributo(padre.getName() + "_FK", false, padre.getName());
//            tabla = listaTablas.get(e.getName());
//            tabla.addAtributo(clavePK.getNombre(), clavePK);
//            tabla.setClaves(clavePK.getNombre());
//            tabla.addAtributo(contenido.getNombre(), contenido);
//            tabla.addAtributo(claveFK.getNombre(), claveFK);
        }
    }

    /***
     * @param un String correspondiente a la clave foranea.
     * @return un String correspondiente a la declaracion de una
     * clave foranea en el scrip sql.
     */
    public static String processIDRefs(String idRefName) {
        Tabla tabla = listaTablas.get(idRefName);
        String claveFK;

        claveFK = "\tFOREIGN KEY (" + idRefName + "_FK) "
                + "REFERENCES " + idRefName + "(" + tabla.getClavePK()
                + "),\n";

        return claveFK;
    }

    /**
     * @param Element e
     * @return Cierto si el elemento es procesable.
     */
    public static void processPcdataAttribute(Element e, Tabla tabla) {

        String nombreAtributo = e.getName() + "_pcdata";
        tabla.addAtributo(nombreAtributo, new Atributo(nombreAtributo, false));
    }

    public static boolean esHijo(DTDdocument dtd, Element e) {
        Enumeration elements = dtd.getElements();

        //Recorre todos los elementos del dtd.
        while (elements.hasMoreElements()) {
            Element element = (Element) elements.nextElement();
            //Si tiene hijos que son elementos.

            if (element.isElemOnlyContent()) {
                ContentParticle cpAux = element.getContent();
                //Si tiene varios hijos.

                if (cpAux.isSeqType() || cpAux.isChoiceType()) {
                    Enumeration children = cpAux.getChildren();
                    //Recorre los hijos y comparo los nombres.

                    while (children.hasMoreElements()) {
                        ContentParticle child = (ContentParticle) children.nextElement();

                        if (child.isReferenceType() && child.getReference().equals(e.getName())) {
                            return true;
                        }
                        children.nextElement();
                    }
                } //Si tiene solo un hijo comparo los nombres.
                else if (cpAux.isReferenceType() && cpAux.getReference().equals(e.getName())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @return verdadero si es pcdata
     * @param Element
     **/
    public static boolean esPcdata(Element e) {
        return e.isMixedContent();
    }

    /**
     * @param ContentParticle
     * @return verdadero si no tiene hijos (es #PCDATA.)
     */
    public static boolean esPcdata(ContentParticle cp) {

        return !cp.isChoiceType() && !cp.isSeqType();
    }

    /* Si no tiene padre , no tiene ID y sus hijos tienen ID o son PCDATA */
    public static int esRelacion(DTDdocument dtd, Element e) {
        int cont = 0;
        String nombre = "";
        int isRelacion = 0;
        Hashtable atribMod = new Hashtable();

        if (!esHijo(dtd, e) && !esEntidad(dtd, e)) {

            //Si tiene mas de 1 hijo
            if (e.isElemOnlyContent()) {
                ContentParticle cp = e.getContent();
                if (cp.isSeqType()) {
                    Enumeration enu = cp.getChildren();
                    while (enu.hasMoreElements()) {
                        ContentParticle conpar = (ContentParticle) enu.nextElement();
                        nombre = conpar.getReference();
                        Element elem = dtd.getElement(nombre);

                        if (esEntidad(dtd, elem)) {
                            cont++;
                        } else if (!esEntidad(dtd, elem)) {

                            if (!esPcdata(elem)) {
                                return 0;
                            }

                            //Si tiene * o + modifico ocurrencia
                            if (conpar.isOneOrMoreOccurances()) {
                                isRelacion = -1;
                                atribMod.put(cp, elem);
                                //    conpar.setOneOccurance();
                            }
                            if (conpar.isZeroOrMoreOccurances()) {
                                isRelacion = -1;
                                atribMod.put(cp, elem);
                                //    conpar.setZeroOrOneOccurance();
                            }

                        }
                    }
                    //Si tiene mas de 2 hijos entidades
                    if (cont >= 2) {
                        if (isRelacion == -1) {
                            Enumeration enumAtrib = atribMod.keys();
                            while (enumAtrib.hasMoreElements()) {
                                ContentParticle contentP = (ContentParticle) enumAtrib.nextElement();

                                if (contentP.isOneOrMoreOccurances()) {
                                    contentP.setOneOccurance();
                                }
                                if (contentP.isZeroOrMoreOccurances()) {
                                    contentP.setZeroOrOneOccurance();
                                }
                            }
                        } else {
                            isRelacion = 1;
                        }
                    }
                }
            }
        }
        return isRelacion;
    }

    public static void readXML(String nombre_xml) throws Exception {

        IXMLParser parser = XMLParserFactory.createDefaultXMLParser();
        IXMLReader reader = StdXMLReader.fileReader(nombre_xml);
        parser.setReader(reader);
        IXMLElement xml = (IXMLElement) parser.parse();

        Enumeration<IXMLElement> relacionesXML = xml.enumerateChildren();
        IXMLElement relacionXML;
        while (relacionesXML.hasMoreElements()) {
            relacionXML = relacionesXML.nextElement();
            // chequeo si la relacion existe en mis estructuras
            if (!relacionesER.containsKey(relacionXML.getName())) {
                // TODO generar error, el elemento XML, que representa una
                // tupla de relacion, tiene mas de dos elementos
                continue;
            }
            procesarXMLRelacion(relacionXML);
        }

        XMLWriter writer = new XMLWriter(System.out);
        writer.write(xml, true);

    }

    private static void procesarXMLRelacion(IXMLElement relacionXML) {
        System.out.println("Procesando la relacion: " + relacionXML.getName() + "\n");

        RelacionER relacionER = relacionesER.get(relacionXML.getName());
        IXMLElement elementoXML01 = relacionXML.getFirstChildNamed(relacionER.getNombreEntidad01());
        IXMLElement elementoXML02 = relacionXML.getFirstChildNamed(relacionER.getNombreEntidad02());

        // chequeo si los nombres de los elementos hijos corresponden
        // con los que tengo en mi estructura de relacionER
        if (elementoXML01 == null || elementoXML02 == null) {
            // TODO generar error, los hijos no corresponden con la relacion
            return;
        }

        Tabla tablaEntidad01 = listaTablas.get(elementoXML01.getName());
        Tabla tablaEntidad02 = listaTablas.get(elementoXML02.getName());
        String tablaEntidad01Clave = tablaEntidad01.getClavePK().iterator().next();
        String tablaEntidad02Clave = tablaEntidad02.getClavePK().iterator().next();

        if (!elementoXML01.hasAttribute(tablaEntidad01Clave)) {
            // TODO generar error, el primer elemento no tiene el identificador
            return;
        }
        if (!elementoXML02.hasAttribute(tablaEntidad02Clave)) {
            // TODO generar error, el segundo elemento no tiene el identificador
            return;
        }

        Tabla tablaRelacion = listaTablas.get(relacionXML.getName());
        HashMap<String, String> entidad01 = insertarEntidad(tablaEntidad01, elementoXML01);
        HashMap<String, String> entidad02 = insertarEntidad(tablaEntidad02, elementoXML02);

        if (tablaRelacion != null) {
            // la relacion NO fue absorbida por alguna entidad
            // TODO chequar restricciones, luego insertar
        } else if (tablaEntidad01.hasAtributo(tablaEntidad02.getNombre())) {
            // la relacion fue absorbida por la entidad 01, es decir,
            // tablaEntidad01 apunta hacia tablaEntidad02
            if (entidad01.containsKey(tablaEntidad02.getNombre())) {
                // TODO error, la fila ya tiene un foreign key apuntando en esa columna
                return;
            } else {
                // extraigo y coloco el apuntador
                String elemento02Clave = elementoXML02.getAttribute(tablaEntidad02Clave, null);
                entidad01.put(tablaEntidad02.getNombre(), elemento02Clave);
            }
        } else if (tablaEntidad02.hasAtributo(tablaEntidad01.getNombre())) {
            // la relacion fue absorbida por la entidad 02
            // tablaEntidad02 apunta hacia tablaEntidad01
            if (entidad02.containsKey(tablaEntidad01.getNombre())) {
                // TODO error, la fila ya tiene un foreign key apuntando en esa columna
                return;
            } else {
                // extraigo y coloco el apuntador
                String elemento01Clave = elementoXML01.getAttribute(tablaEntidad01Clave, null);
                entidad02.put(tablaEntidad01.getNombre(), elemento01Clave);
            }
        }

        // TODO procesar atributos de la relacion
    }

    /**
     * Chequea si existe el elemento xml, en caso que no exista lo inserta en la tabla
     * @param tablaEntidad Tabla SQL en la que se insertara
     * @param elementoXML Elemento XML a insertar
     * @return En caso que exista el elemento retorna la fila, en caso contrario retorna el elemento insertado
     */
    private static HashMap<String, String> insertarEntidad(Tabla tablaEntidad, IXMLElement elementoXML) {
        HashMap<String, String> entidad;

        String columnaClave = tablaEntidad.getClavePK().iterator().next();
        String elementoClave = elementoXML.getAttribute(columnaClave, null);

        entidad = tablaEntidad.getFilaConClave(columnaClave, columnaClave);
        if (entidad != null) {
            return entidad;
        }
        entidad = new HashMap<String, String>();

        // Primero agrego todos los atributos (ATTLIST)
        Enumeration<String> nombreAtributos = elementoXML.enumerateAttributeNames();
        while(nombreAtributos.hasMoreElements()) {
            String nombreAttributo = nombreAtributos.nextElement();
            entidad.put(nombreAttributo, elementoXML.getAttribute(nombreAttributo, null));
        }
        
        // TODO agregar los hijos del elemento
        return entidad;
    }
}
